package socket;


import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/*
 * 服务端
 * */
public class Server {
    /*
     * java.net.ServerSocket
     * 运行在服务端的ServerSocket的主要工作
     * 1.向系统申请对外的服务端口，客户端就是通过这个端口与服务器建立间接的
     * 2.监听服务器端口，一旦一个客户端建立链接立即接收并获取一个Socket实例与之交互
     * */
    private ServerSocket serverSocket;
    //存放所有客户端的输出流，用于将客户端发送过来的信息进行广播
    private List<PrintWriter> allOut = new ArrayList<PrintWriter>();
    private ArrayList arrayList;

    public Server() {
        try {
            /*
             * 当前设定当前服务端所申请的服务器端口号
             * 客户端中也是通过服务器IP地址和服务端所使用的8088端口完成客户端和服务端的通信
             * */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        System.out.println("等待客户端的链接....");
        try {
            /*
             * Socket用来和远程计算机建立链接
             * ServerSocket中一个重要的方法:
             * Scoket accept();
             * 接受客户端的链接，该方法是一个阻塞方法，调用忽进入阻塞状态(程序不向下运行)
             * 直到一个客户端建立链接，此时该方法会立即返回一个Socket与客户端形成对等关系，并利用这个Socket与客户端交互
             * */
            while (true) {
                System.out.println("服务端正在等待客户端链接");
                /*
                 * 当客户端对服务端申请访问是，服务端接收到客户端的申请并返回一个Socket对象
                 * 当前socket中存放的就是客户端传递过来的实例
                 * */
                Socket socket = serverSocket.accept();
                System.out.println("与客户端连接成功");

                Runnable r = new ClientHandler(socket);
                Thread t = new Thread(r);
                t.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /*
     * 该线程任务负责与指定的客户端进行交互
     * */
    private class ClientHandler implements Runnable {

        private Socket socket;
        private String host; //记录客户端的ip地址

        public ClientHandler(Socket socket) {
            this.socket = socket;
            //通过socket获取远端计算机(客户端)的IP地址信息
            //getInetAddress()获取
            host = socket.getInetAddress().getHostAddress();
        }

        @Override
        public void run() {
            //提高pw的作用域
            PrintWriter pw = null;
            try {
                /*
                 * Socker中要的方法
                 * InputStream getInputStream();
                 * 通过Socket获取的输入流可以读取远端计算机发送过来的数据
                 * */

                //通过socket获取输入流(读取客户端发送过来的数据)
                InputStream inputStream = socket.getInputStream();
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

                //通过socket获取输出流(向客户端写出数据)
                OutputStream os = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw, true);

                synchronized (allOut) {
                    //将该输出流存放到共享输出流的集合当中，以便其他的ClinetHandler可以将消息发送给这个客户端
                    allOut.add(pw);
                    sendMessage(host + "上线了,当前在线人数:" + allOut.size());
                }

                //利用循环读取客户端发送过来的每行字符串(readLine()按行读取字符串)
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    sendMessage(host + "说:" + line);
//                    将消息发送给客户端
//                    pw.println(host+"说:"+line);
                }

            } catch (IOException e) {

            } finally {
                /*
                 * 在此处将allOut进行同步操作的话，可以避免集合中会出现的并发安全的问题
                 * 而且 因为在allOut.add方法出也被synchronized块包围，所以当前的allout.remove方法和add方法时互斥的 这两个只能有一个在执行
                 * */
                synchronized (allOut) {
                    //此处统一处理该客户端下线后的工作
                    //将GIA用户端的输出流从allout中删除掉
                    allOut.remove(pw);
                }
                sendMessage(host + "下线了,当前在线人数" + allOut.size());
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /*
         * 将给定的消息发送给所有客户端
         * */
        private void sendMessage(String line) {
            /*
             * 因为集合在遍历的时候是不可以对集合进行增删操作的
             * 所以要将遍历集合的代码也要放在synchronized代码块中 并且监视对象也是allout的集合
             * 将此处的遍历和上述的add和remove方法设置成互斥 他们三个块中同一时间只能有一个在运行
             * */
            synchronized (allOut) {
                System.out.println(line);
                for (PrintWriter p : allOut
                ) {
                    System.out.println(p.toString());
                    p.println(line);
                }
            }
        }

    }
}
